﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Linq;
using XLModbus.Enums;
using XLModbus.Tools;

namespace XLModbus.Plcs.Modbus
{
    public class ModbusTcp : IModbus
    {
        private TcpClient Client { get; set; }
        private NetworkStream Stream { get; set; }
        private int _port;
        private int _timeout;
        private string _host;
        private byte _unit;
        private int _lock = 0;
        public ModbusTcp(string ip, int port, byte unit, int timeout = 3000)
        {
            _port = port;
            _host = ip;
            _unit = unit;
            _timeout = timeout;
            Client = new TcpClient();
        }

        public bool Close()
        {
            try
            {
                if (Client.Connected)
                {
                    Client.Close();
                }
                Stream?.Dispose();
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }
        public bool Open()
        {
            if (!Client.Connected)
            {
                //Keep Alive功能的实现
                var ka = new List<byte>(sizeof(uint) * 3);
                ka.AddRange(BitConverter.GetBytes(1u));
                ka.AddRange(BitConverter.GetBytes(45000u));
                ka.AddRange(BitConverter.GetBytes(5000u));
                Client.Client.IOControl(IOControlCode.KeepAliveValues, ka.ToArray(), null);
                Client.Connect(_host, _port);
                Stream = Client.GetStream();
            }
            return true;
        }

        /// <summary>
        /// 发送数据与请求数据
        /// </summary>
        /// <param name="iCommand"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        private byte[] Execute(byte[] iCommand)
        {
            NetworkStream ns = Stream;
            ns.Write(iCommand, 0, iCommand.Length);
            ns.Flush();

            using (var ms = new MemoryStream())
            {
                var buff = new byte[256];
                do
                {
                    int sz = ns.Read(buff, 0, buff.Length);
                    if (sz == 0)
                    {
                        UnLock();
                        throw new Exception("断开连接");
                    }
                    ms.Write(buff, 0, sz);
                } while (ns.DataAvailable);
                return ms.ToArray();
            }
        }

        /// <summary>
        /// 检测返回的报文是否正确
        /// </summary>
        /// <param name="bytes">完整的报文</param>
        /// <returns></returns>
        private bool IsIncorrectResponse(byte[] bytes)
        {
            if (bytes.Length < 9) { return false; }
            int btCount = bytes.Length - 6;
            int length = BitConverter.ToInt16(new byte[] { bytes[5], bytes[4] }, 0);
            if (bytes[7] > 0x80) { UnLock(); throw new Exception($"异常功能码：[{(MudbosErrorCode)bytes[8]}]"); }
            if (btCount != length) { UnLock(); throw new Exception("报文不完整"); }
            return false;
        }

        /// <summary>
        /// 校验报文
        /// </summary>
        /// <param name="iCommand"></param>
        /// <returns></returns>
        private byte[] TryExecution(byte[] iCommand)
        {
            byte[] rtResponse;
            int tCount = 3;
            do
            {
                rtResponse = Execute(iCommand);
                --tCount;
                if (tCount < 0)
                {
                    UnLock();
                    throw new Exception("无法获得正确的报文!!!");
                }
            } while (IsIncorrectResponse(rtResponse));
            return rtResponse;
        }

        /// <summary>
        /// 设置报文
        /// </summary>
        /// <param name="iMainCommand"></param>
        /// <param name="iSubCommand"></param>
        /// <param name="iData"></param>
        /// <returns></returns>
        private byte[] SetCommand(ModbusEnums iMainCommand, byte[] iData)
        {
            List<byte> data = new List<byte>() { 0x97, 0x76, 0x00, 0x00 };
            byte[] ldata = BitConverter.GetBytes((ushort)(iData.Length + 2));
            data.Add(ldata[1]);
            data.Add(ldata[0]);
            data.Add(_unit);
            data.Add((byte)iMainCommand);
            data.AddRange(iData);
            return data.ToArray();
        }

        private void Lock()
        {
            while (System.Threading.Interlocked.Exchange(ref _lock, 1) != 0)
            {
                _ = 0; _ = 0; _ = 0; _ = 0;
                _ = 0; _ = 0; _ = 0; _ = 0;
                _ = 0; _ = 0; _ = 0; _ = 0;
                _ = 0; _ = 0; _ = 0; _ = 0;
            }
        }
        private void UnLock() => System.Threading.Interlocked.Exchange(ref _lock, 0);

        public bool SetRegister<TValue>(int iaddress, List<object> value) where TValue : struct
        {
            Lock();
            Type type = typeof(TValue);
            List<byte> command = new List<byte>()
            {
                (byte)(iaddress >> 8),
                (byte)iaddress,
                0x00,
            };
            int length = value.Count;
            if (type.Name == "Int16" || type.Name == "UInt16")
            {
                command.Add((byte)length);
                command.Add((byte)(length * 2));
            }
            if (type.Name == "Int32" || type.Name == "UInt32" || type.Name == "Single")
            {
                command.Add((byte)(length * 2));
                command.Add((byte)(length * 4));
            }
            if (type.Name == "Double" || type.Name == "Int64" || type.Name == "UInt64")
            {
                command.Add((byte)(length * 4));
                command.Add((byte)(length * 8));
            }
            foreach (var item in value)
            {
                byte[] data = null;
                switch (type.Name)
                {
                    case "Int16": data = BitConverter.GetBytes((short)item); break;
                    case "UInt16": data = BitConverter.GetBytes((ushort)item); break;
                    case "Int32": data = BitConverter.GetBytes((int)item); break;
                    case "UInt32": data = BitConverter.GetBytes((uint)item); break;
                    case "Single": data = BitConverter.GetBytes((float)item); break;
                    case "Double": data = BitConverter.GetBytes((double)item); break;
                    case "Int64": data = BitConverter.GetBytes((long)item); break;
                    case "UInt64": data = BitConverter.GetBytes((ulong)item); break;
                    default: UnLock(); throw new Exception("未实现该类型!!!");
                }
                for (int i = 0; i < data.Length; i = i + 2)
                {
                    command.Add(data[i + 1]);
                    command.Add(data[i]);
                }
            }
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_MULTIPLE_REGISTER, command.ToArray());
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            return rtResponse[10] == sdCommand[10] && rtResponse[11] == sdCommand[11];
        }

        public bool SetRegister(int iaddress, object value)
        {
            Lock();
            Type type = value.GetType();
            List<byte> command = new List<byte>()
            { 
                (byte)(iaddress >> 8),
                (byte)iaddress,
            };
            ModbusEnums enums = ModbusEnums.WRITE_SINGLE_REGISTER;
            byte[] data = null;
            if (type.Name == "Int32" || type.Name == "UInt32" || type.Name == "Single")
            {
                enums = ModbusEnums.WRITE_MULTIPLE_REGISTER;
                command.Add(0);
                command.Add(0x02);
                command.Add(0x04);
            }
            if (type.Name == "Double" || type.Name == "Int64" || type.Name == "UInt64")
            {
                enums = ModbusEnums.WRITE_MULTIPLE_REGISTER;
                command.Add(0);
                command.Add(0x04);
                command.Add(0x08);
            }
            switch (type.Name)
            {
                case "Int16": data = BitConverter.GetBytes((short)value); break;
                case "UInt16": data = BitConverter.GetBytes((ushort)value); break;
                case "Int32": data = BitConverter.GetBytes((int)value); break;
                case "UInt32": data = BitConverter.GetBytes((uint)value); break;
                case "Single": data = BitConverter.GetBytes((float)value); break;
                case "Double": data = BitConverter.GetBytes((double)value); break;
                case "Int64": data = BitConverter.GetBytes((long)value); break;
                case "UInt64": data = BitConverter.GetBytes((ulong)value); break;
                default: UnLock(); throw new Exception("未实现该类型!!!");
            }
            for (int i = 0; i < data.Length; i = i + 2)
            {
                command.Add(data[i + 1]);
                command.Add(data[i]);
            }
            byte[] sdCommand = SetCommand(enums, command.ToArray());
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            return rtResponse[10] == command[2] && rtResponse[11] == command[3];
        }

        public bool SetBitRegister(int iaddress, int offset, bool bl)
        {
            Lock();
            int value = bl ? 0x01 << offset : 0;
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(value >> 8);
            command[3] = (byte)value;
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            return rtResponse[10] == command[2] && rtResponse[11] == command[3];
        }

        public bool SetBitRegister(int iaddress, ushort old_value, int offset, bool bl)
        {
            Lock();
            ushort value = (ushort)(old_value | (bl ? (0x01 << offset) : 0));
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(value >> 8);
            command[3] = (byte)value;
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            return rtResponse[10] == command[2] && rtResponse[11] == command[3];
        }

        public bool SetBitsRegister(int iaddress, List<bool> value)
        {
            Lock();
            ushort values = value.BoolsToUShort();
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(values >> 8);
            command[3] = (byte)values;
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            return rtResponse[10] == command[2] && rtResponse[11] == command[3];
        }

        public bool SetRegister(int iaddress, ushort[] value)
        {
            Lock();
            int count = value.Length;
            List<byte> command = new List<byte>()
            {
                (byte)(iaddress >> 8),
                (byte)iaddress,
                (byte)(count >> 8),
                (byte)count,
                (byte)(value.Length * 2),
            };
            foreach (var item in value)
            {
                command.Add((byte)(item >> 8));
                command.Add((byte)item);
            }
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_MULTIPLE_REGISTER, command.ToArray());
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            return rtResponse[10] == sdCommand[10] && rtResponse[11] == sdCommand[11];
        }

        public bool ReadBitRegister(int iaddress, int offset = 0) => ReadBitsRegister(iaddress, 16)[offset];

        public List<bool> ReadBitsRegister(int iaddress, int count)
        {
            int length = (count / 16) + ((count % 16 > 0) ? 1 : 0);
            List<bool> value = new List<bool>();
            List<ushort> list = ReadRegister(iaddress, length);
            for (int i = 0; i < list.Count; i++)
            {
                for (int j = 0; j < 16; j++)
                {
                    value.Add(((list[i] >> j) & 0x01) == 0x01);
                    if (value.Count == count) { return value; }
                }
            }
            return value;
        }

        public TValue ReadRegister<TValue>(int iaddress) where TValue : struct
        {
            Lock();
            Type type = typeof(TValue);
            object value = null;
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            int count = 1;
            if (type.Name == "Int32" || type.Name == "UInt32" || type.Name == "Single") { count = 2; }
            if (type.Name == "Double" || type.Name == "Int64" || type.Name == "UInt64") { count = 4; }
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_HOLDING_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            switch (type.Name)
            {
                case "Int16": value = ByteToShort(rtResponse[9], rtResponse[10]); break;
                case "UInt16": value = ByteToUShort(rtResponse[9], rtResponse[10]); break;
                case "Int32": value = ByteToInt(rtResponse.Skip(9).ToArray()); break;
                case "UInt32": value = ByteToUInt(rtResponse.Skip(9).ToArray()); break;
                case "Single": value = BitConverter.ToSingle(new byte[] { rtResponse[10], rtResponse[9], rtResponse[12], rtResponse[11] }, 0); break;
                case "Double": value = BitConverter.ToDouble(
                    new byte[] { 
                        rtResponse[10], rtResponse[9], rtResponse[12], rtResponse[11], 
                        rtResponse[14], rtResponse[13], rtResponse[16], rtResponse[15] 
                    }, 0); break;
                case "Int64": value = ByteToInt(rtResponse.Skip(9).ToArray()) + (ByteToInt(rtResponse.Skip(13).ToArray()) << 32); break;
                case "UInt64": value = ByteToUInt(rtResponse.Skip(9).ToArray()) + (ByteToUInt(rtResponse.Skip(13).ToArray()) << 32); break;
                default: UnLock(); throw new Exception("未实现该类型!!!");
            }
            return (TValue)value;
        }

        private ushort ByteToUShort(byte byte1, byte byte2) => (ushort)((byte1 << 8) + byte2);
        private short ByteToShort(byte byte1, byte byte2) => (short)((byte1 << 8) + byte2);
        private int ByteToInt(byte[] bytes) => (ByteToShort(bytes[2], bytes[3]) << 16) + ByteToShort(bytes[0], bytes[1]);
        private int ByteToUInt(byte[] bytes) => (ByteToUShort(bytes[2], bytes[3]) << 16) + ByteToUShort(bytes[0], bytes[1]);

        public List<ushort> ReadRegister(int iaddress, int count)
        {
            Lock();
            List<ushort> value = new List<ushort>();
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_HOLDING_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            for (int i = 9; i < rtResponse.Length; i = i + 2)
            {
                value.Add((ushort)((rtResponse[i] << 8) + rtResponse[i + 1]));
            }
            return value;
        }

        public T ReadClass<T>(int iaddress) where T : class
        {
            Lock();
            object obj = Activator.CreateInstance<T>();
            if (obj == null) { UnLock(); throw new Exception("泛型不能为NULL"); }
            int length = (int)(ClassHelper.GetClassSize(obj) / 2);
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(length >> 8);
            command[3] = (byte)length;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_HOLDING_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            List<byte> sort = new List<byte>();
            for (int i = 9; i < rtResponse.Length; i = i + 2)
            {
                sort.Add(rtResponse[i + 1]);
                sort.Add(rtResponse[i]);
            }
            ClassHelper.FromBytes(obj, sort.ToArray());
            return (T)obj;
        }

        public ushort ReadInputRegister(int iaddress) => ReadInputRegister(iaddress, 1)[0];
        public List<ushort> ReadInputRegister(int iaddress, int count)
        {
            Lock();
            List<ushort> list = new List<ushort>();
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_INPUT_REGISTER, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            for (int i = 9; i < rtResponse.Length; i = i + 2)
            {
                list.Add((ushort)((rtResponse[i] << 8) + rtResponse[i + 1]));
            }
            return list;
        }
        public bool ReadInputBitRegister(int iaddress, int offset = 0) => ReadInputBitsRegister(iaddress, 16)[offset];

        public List<bool> ReadInputBitsRegister(int iaddress, int count)
        {
            int length = (count / 16) + ((count % 16 > 0) ? 1 : 0);
            List<bool> value = new List<bool>();
            List<ushort> list = ReadInputRegister(iaddress, length);
            for (int i = 0; i < list.Count; i++)
            {
                for (int j = 0; j < 16; j++)
                {
                    value.Add(((list[i] >> j) & 0x01) == 0x01);
                    if (value.Count == count) { return value; }
                }
            }
            return value;
        }

        public bool SetCoil(int iaddress, bool bl)
        {
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(bl ? 0xFF : 0x00);
            command[3] = 0x00;
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_SINGLE_COIL, command);
            byte[] rtResponse = TryExecution(sdCommand);
            return rtResponse[10] == (byte)(bl ? 0xFF : 0x00);
        }

        public bool SetCoil(int iaddress, bool[] bl)
        {
            Lock();
            byte[] value = new List<bool>(bl).BoolsToByte();
            List<byte> command = new List<byte>()
            {
                (byte)(iaddress >> 8),
                (byte)iaddress,
                (byte)(bl.Length >> 8),
                (byte)bl.Length,
                (byte)value.Length,
            };
            command.AddRange(value);
            byte[] sdCommand = SetCommand(ModbusEnums.WRITE_MULTIPLE_COIL, command.ToArray());
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            return sdCommand[11] == rtResponse[11];
        }

        public List<bool> GetCoil(int iaddress, int count = 0)
        {
            Lock();
            List<bool> value = new List<bool>();
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_COIL_STATUS, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            for (int i = 9; i < rtResponse.Length; i++)
            {
                for (int j = 0; j < 8; j++)
                {
                    value.Add(((rtResponse[i] >> j) & 0x01) == 0x01);
                    if (value.Count == count) { return value; }
                }
            }
            return value;
        }

        public bool GetCoil(int iaddress) => GetCoil(iaddress, 1)[0];

        public List<bool> GetDiscreteInput(int iaddress, int count = 0)
        {
            Lock();
            List<bool> value = new List<bool>();
            byte[] command = new byte[4];
            command[0] = (byte)(iaddress >> 8);
            command[1] = (byte)iaddress;
            command[2] = (byte)(count >> 8);
            command[3] = (byte)count;
            byte[] sdCommand = SetCommand(ModbusEnums.READ_INPUT_STATUS, command);
            byte[] rtResponse = TryExecution(sdCommand);
            UnLock();
            for (int i = 9; i < rtResponse.Length; i++)
            {
                for (int j = 0; j < 8; j++)
                {
                    value.Add(((rtResponse[i] >> j) & 0x01) == 0x01);
                    if (value.Count == count) { return value; }
                }
            }
            return value;
        }

        public bool GetDiscreteInput(int iaddress) => GetDiscreteInput(iaddress, 1)[0];
    }
}
